/**
 * This file is part of aion-emu <aion-emu.com>.
 *
 *  aion-emu is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  aion-emu is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with aion-emu.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aionemu.gameserver.world;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;

import javolution.util.FastList;
import javolution.util.FastMap;

import org.apache.log4j.Logger;

import com.aionemu.commons.utils.Rnd;
import com.aionemu.gameserver.configs.main.OptionsConfig;
import com.aionemu.gameserver.dataholders.DataManager;
import com.aionemu.gameserver.model.gameobjects.AionObject;
import com.aionemu.gameserver.model.gameobjects.NpcWhichNotGatherable;
import com.aionemu.gameserver.model.gameobjects.VisibleObject;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.team.group.PlayerGroup;
import com.aionemu.gameserver.model.templates.WorldMapTemplate;
import com.aionemu.gameserver.model.templates.portal.EntryPoint;
import com.aionemu.gameserver.model.templates.portal.PortalTemplate;
import com.aionemu.gameserver.model.templates.quest.NpcQuestData;
import com.aionemu.gameserver.questEngine.QuestEngine;
import com.aionemu.gameserver.services.TeleportService;
import com.aionemu.gameserver.spawnengine.SpawnEngine;
import com.aionemu.gameserver.utils.ThreadPoolManager;
import com.aionemu.gameserver.world.exceptions.DuplicateAionObjectException;

/**
 * World map instance object.
 * 
 * @author -Nemesiss-
 * 
 */
public class WorldMapInstance
{
	/**
	 * Logger for this class.
	 */
	private static final Logger	log			= Logger.getLogger(WorldMapInstance.class);

	/**
	 * Size of region
	 */
	public static final int		regionSize	= OptionsConfig.REGION_SIZE;

	protected static final int	offset		= 1000;

	public synchronized static WorldMapInstance getNextAvailableInstance(int worldId)
	{
		WorldMap map = World.getInstance().getWorldMap(worldId);

		if (!map.isInstanceType())
			throw new UnsupportedOperationException("Invalid call for next available instance  of " + worldId);

		int nextInstanceId = map.getNextInstanceId();

		log.info("Creating new instance: " + worldId + " " + nextInstanceId);

		WorldMapInstance worldMapInstance = map.addInstance(nextInstanceId);
		worldMapInstance.startInstanceChecker();
		SpawnEngine.getInstance().spawnInstance(worldId, worldMapInstance.getInstanceId());

		return worldMapInstance;
	}

	/**
	 * Instance will be destroyed All players moved to bind location All objects - deleted
	 */
	private void destroyInstance(WorldMapInstance instance)
	{
		instance.getEmptyInstanceTask().cancel(false);

		int worldId = instance.getMapId();
		int instanceId = instance.getInstanceId();

		WorldMap map = World.getInstance().getWorldMap(worldId);
		map.removeWorldMapInstance(instanceId);

		log.info("Destroying instance:" + worldId + " " + instanceId);

		for (VisibleObject obj : instance.getAllWorldMapObjects())
		{
			if (obj instanceof Player)
			{
				Player player = (Player) obj;
				PortalTemplate portal = DataManager.PORTAL_DATA.getInstancePortalTemplate(worldId, player.getObjectTemplate().getRace());
				moveToEntryPoint((Player) obj, portal, true);
			}
			else
			{
				obj.delete();
			}
		}
	}

	public static boolean isInstanceExist(int worldId, int instanceId)
	{
		return World.getInstance().getWorldMap(worldId).getWorldMapInstanceById(instanceId) != null;
	}

	private static void moveToEntryPoint(Player player, PortalTemplate portalTemplate, boolean useTeleport)
	{
		EntryPoint entryPoint = null;
		List<EntryPoint> entryPoints = portalTemplate.getEntryPoint();

		for (EntryPoint point : entryPoints)
		{
			if (point.getRace() == null || point.getRace().equals(player.getObjectTemplate().getRace()))
			{
				entryPoint = point;
				break;
			}
		}

		if (entryPoint == null)
		{
			log.warn("Entry point not found for " + player.getObjectTemplate().getRace() + " " + player.getWorldId());
			return;
		}

		if (useTeleport)
		{
			TeleportService.teleportTo(player, entryPoint.getMapId(), 1, entryPoint.getX(), entryPoint.getY(), entryPoint.getZ(), 0);
		}
		else
		{
			WorldMap worldMap = World.getInstance().getWorldMap(entryPoint.getMapId());
			if (worldMap != null && worldMap.getWorldMapInstanceById(1) != null)
				World.getInstance().setPosition(player, entryPoint.getMapId(), 1, entryPoint.getX(), entryPoint.getY(), entryPoint.getZ(), player.getHeading());
			else
				TeleportService.moveToBindLocation(player, false);
		}

	}

	public static WorldMapInstance getRegisteredInstance(int worldId, int objectId)
	{
		for (WorldMapInstance instance : World.getInstance().getWorldMap(worldId).getAllWorldMapInstances())
		{
			if (instance.isRegistered(objectId))
				return instance;
		}
		return null;
	}

	public static void onPlayerLogin(Player player)
	{
		int worldId = player.getWorldId();

		WorldMapTemplate worldTemplate = DataManager.WORLD_MAPS_DATA.getTemplate(worldId);
		if (worldTemplate.isInstance())
		{
			PortalTemplate portalTemplate = DataManager.PORTAL_DATA.getInstancePortalTemplate(worldId, player.getObjectTemplate().getRace());

			if (portalTemplate == null)
			{
				log.error("No portal template found for " + worldId);
				return;
			}

			int lookupId = player.getObjectId();
			if (portalTemplate.isGroup() && player.getPlayerGroup() != null)
			{
				lookupId = player.getPlayerGroup().getObjectId();
			}

			WorldMapInstance registeredInstance = getRegisteredInstance(worldId, lookupId);
			if (registeredInstance != null)
			{
				World.getInstance().setPosition(player, worldId, registeredInstance.getInstanceId(), player.getX(), player.getY(), player.getZ(),
						player.getHeading());
				return;
			}

			moveToEntryPoint(player, portalTemplate, false);
		}
		else
		{
			int instanceId = World.getInstance().getWorldMap(worldId).getWorldMapInstance().getInstanceId();
			
			World.getInstance().setPosition(player, worldId, instanceId, player.getX(), player.getY(), player.getZ(),
					player.getHeading());
		}
	}

	/**
	 * WorldMap witch is parent of this instance.
	 */
	private final WorldMap						parent;
	/**
	 * Map of active regions.
	 */
	protected MapRegion[][]						regions;

	/**
	 * All objects spawned in this world map instance
	 */
	private final Map<Integer, VisibleObject>	worldMapObjects		= new FastMap<Integer, VisibleObject>().shared();

	/**
	 * All players spawned in this world map instance
	 */
	private final Map<Integer, Player>			worldMapPlayers		= new FastMap<Integer, Player>().shared();

	private final Set<Integer>					registeredObjects	= Collections.newSetFromMap(new FastMap<Integer, Boolean>().shared());

	private PlayerGroup							registeredGroup		= null;

	private Future<?>							emptyInstanceTask	= null;

	/**
	 * Id of this instance (channel)
	 */
	private final int							instanceId;

	private final FastList<Integer> questIds = new FastList<Integer>();
	/**
	 * Constructor.
	 * 
	 * @param parent
	 */
	public WorldMapInstance(WorldMap parent, int instanceId)
	{
		this.parent = parent;
		this.instanceId = instanceId;
		initMapRegions();
	}

	/**
	 * Return World map id.
	 * 
	 * @return world map id
	 */
	public Integer getMapId()
	{
		return getParent().getMapId();
	}

	protected void initMapRegions()
	{
		this.regions = new MapRegion[parent.getWorldSize() / regionSize + 1][parent.getWorldSize() / regionSize + 1];
		int size = parent.getWorldSize() / regionSize;
		//Create all mapRegion
		for (int x = 0; x <= size; x++)
		{
			for (int y = 0; y <= size; y++)
			{
				regions[x][y] = new MapRegion(this, false);
			}
		}

		// Add Neighbour
		for (int x = 0; x <= size; x++)
		{
			for (int y = 0; y <= size; y++)
			{
				MapRegion mapRegion = regions[x][y];
				for (int x2 = x - 1; x2 <= x + 1; x2++)
				{
					for (int y2 = y - 1; y2 <= y + 1; y2++)
					{
						if ((x2 == x && y2 == y) || x2 == -1 || y2 == -1)
							continue;
						try
						{
							MapRegion neighbour = regions[x2][y2];
							mapRegion.addNeighbourRegion(neighbour);
						}
						catch (ArrayIndexOutOfBoundsException e)
						{
							continue;
						}
						catch (NullPointerException e)
						{
							continue;
						}
					}
				}
			}
		}

		log.debug(this.getMapId() + " Created map regions: " + regions.length);
	}

	/**
	 * Returns WorldMap witch is parent of this instance
	 * 
	 * @return parent
	 */
	public WorldMap getParent()
	{
		return parent;
	}

	/**
	 * Returns MapRegion that contains coordinates of VisibleObject. If the region doesn't exist, it's created.
	 *
	 * @param object
	 *
	 * @return a MapRegion
	 */
	MapRegion getRegion(VisibleObject object)
	{
		return getRegion(object.getX(), object.getY(), object.getZ());
	}

	/**
	 * Returns MapRegion that contains given x,y coordinates. If the region doesn't exist, it's created.
	 *
	 * @param x
	 * @param y
	 * @return a MapRegion
	 */
	MapRegion getRegion(float x, float y, float z)
	{
		MapRegion region = null;
		try
		{
			region = regions[(int) x / regionSize][(int) y / regionSize];
		}
		catch (ArrayIndexOutOfBoundsException e)
		{
			log.warn("MAP REGION: Cord out of world!!! x: " + x + " y: " + y + " z: " + z);
		}
		return region;
	}

	/**
	 * Returs {@link World} instance to which belongs this WorldMapInstance
	 * 
	 * @return World
	 */
	public World getWorld()
	{
		return getParent().getWorld();
	}

	/**
	 * 
	 * @param object
	 */
	public void addObject(VisibleObject object)
	{
		if (worldMapObjects.put(object.getObjectId(), object) != null)
			throw new DuplicateAionObjectException();
		if (object instanceof NpcWhichNotGatherable)
		{
			NpcQuestData data = QuestEngine.getInstance().getNpcQuestData(object.getNpcId());
			if (data != null)
			{
				for (int id : data.getOnQuestStart())
					if (!questIds.contains(id))
						questIds.add(id);
			}
		}
		if (object instanceof Player)
			worldMapPlayers.put(object.getObjectId(), (Player) object);
	}

	/**
	 * 
	 * @param object
	 */
	public void removeObject(AionObject object)
	{
		worldMapObjects.remove(object.getObjectId());
		if (object instanceof Player)
			worldMapPlayers.remove(object.getObjectId());
	}

	/**
	 * @return the instanceIndex
	 */
	public int getInstanceId()
	{
		return instanceId;
	}

	/**
	 * Check player is in instance
	 * 
	 * @param objId
	 * @return
	 */
	public boolean isInInstance(int objId)
	{
		return worldMapPlayers.containsKey(objId);
	}

	public Collection<VisibleObject> getAllWorldMapObjects()
	{
		return worldMapObjects.values();
	}

	public Collection<Player> getAllWorldMapPlayers()
	{
		return worldMapPlayers.values();
	}

	public void registerGroup(PlayerGroup group)
	{
		registeredGroup = group;
		register(group.getObjectId());
	}

	/**
	 * @param objectId
	 */
	public void register(int objectId)
	{
		registeredObjects.add(objectId);
	}

	/**
	 * @return the registeredObjects
	 */
	public int getRegisteredObjectsSize()
	{
		return registeredObjects.size();
	}

	/**
	 * @param objectId
	 * @return
	 */
	public boolean isRegistered(int objectId)
	{
		return registeredObjects.contains(objectId);
	}

	/**
	 * @return the emptyInstanceTask
	 */
	public Future<?> getEmptyInstanceTask()
	{
		return emptyInstanceTask;
	}

	/**
	 * @param emptyInstanceTask
	 *            the emptyInstanceTask to set
	 */
	public void setEmptyInstanceTask(Future<?> emptyInstanceTask)
	{
		this.emptyInstanceTask = emptyInstanceTask;
	}

	/**
	 * @return the registeredGroup
	 */
	public PlayerGroup getRegisteredGroup()
	{
		return registeredGroup;
	}

	/**
	 * 
	 * @return
	 */
	public int playersCount()
	{
		return worldMapPlayers.size();
	}

	private void startInstanceChecker()
	{
		int delay = 60000 + Rnd.get(-10, 10);
		setEmptyInstanceTask(ThreadPoolManager.getInstance().scheduleAtFixedRate(EmptyInstanceChecker(), delay, delay));
	}

	private WorldMapInstance get()
	{
		return this;
	}

	private Runnable EmptyInstanceChecker()
	{
		return new Runnable()
		{
			@Override
			public void run()
			{
				PlayerGroup registeredGroup = getRegisteredGroup();
				if (registeredGroup == null)
				{
					if (playersCount() == 0)
					{
						destroyInstance(get());
						return;
					}
					int mapId = get().getMapId();
					for (Player player : get().getAllWorldMapPlayers())
					{
						if (player.isOnline() && player.getWorldId() == mapId)
							return;
					}
					destroyInstance(get());
				}
				else if (registeredGroup.size() == 0)
				{
					destroyInstance(get());
				}
			}
		};
	}

	public void registerPlayerWithInstance(Player player)
	{
		register(player.getObjectId());
	}

	public void registerGroupWithInstance(PlayerGroup group)
	{
		registerGroup(group);
	}

	/**
	 * @return Returns the questIds.
	 */
	public FastList<Integer> getQuestIds()
	{
		return questIds;
	}

}
